經過 pg mvcc 與 mysql mvcc 的介紹後,
我們知道 SX lock 和 MVCC 都能逹到 Isolation 目的,
但 SX lock 為什麼只加上 ReadView , write 呢?
我們來細細的分析可能會有什麼問題。
兩種 isolation 流派
SX lock 和 MVCC 都能逹到 Isolation 目的,
因為其底層機制不同,所以雖然大家都說自己支持 ACID ,
支持 4 個 isolation level ,其 isolation 的行為卻有所不同
MySQL 和 MSSQL 近年試圖在其 SX lock 架構上再加上 MVCC。
update
,兩次的 select 結果會不同insert
,兩次的 select 結果會不同所以 RR(REPEATABLE READ)本來就不防幻讀,
如果有人跟你說 mysql RR 發生幻讀, 你要跟他說非常正常,
因為每個 db 的 RR 都會發生幻, 而且理應發生。
聽起來似乎都沒有問題,一且都完美的呈現, 但問題在哪裡呢?請聽我娓娓道來。
我們就直接來舉的例子比較清楚
time | tx1 | tx2 | trx_id | readview |
---|---|---|---|---|
1 | begin | begin | 0 | m_ids=[1,2] |
2 | read | 0 | m_ids=[1,2] | |
3 | write | 2 | m_ids=[1,2] | |
4 | commit | 2 | m_ids=[1,2] | |
5 | read | 2 | m_ids=[1] |
假設原資料 num=0
tx2 update num=num+2
REPEATABLE READ 只有 time2 會建立 readview, time5 直接使用 time2 的 readview
//原表
{"num":0,"trx_id": 0}
//ReadView
{ "creator_trx_id": "tx1", "m_ids": [ 1,2 ], "up_limit_id": 1, "low_limit_id": 2}
// 取 {"num":0,"trx_id": 0}
條件 1: creator_trx_id(tx2)=trx_id(0) false
條件 2:
[trx_id<=up_limit_id]<___<low_limit_id
, return true結論: readview 可以利用目前表上的資料,直接讀原表該紀錄的尾巴,即可建立 readview, 記下這張 readview
//原表
{"num":0,"trx_id": 0}<= {"num":0+2,"trx_id": 2}
// ReadView
// 直接抓上次的 {"name":0,"trx_id": 0}
可以清楚地看到, 在 REPEATABLE READ 的 level 下,我們可以正常的讀到同一個結果。
如果把 time5 改成 Write 呢?
我們就直接來舉的例子比較清楚
time | tx1 | tx2 | tx3 | trx_id | readview |
---|---|---|---|---|---|
1 | begin | begin | 0 | m_ids=[1,2] | |
2 | read | 0 | m_ids=[1,2] | ||
3 | write | 2 | m_ids=[2] | ||
4 | commit | 2 | m_ids=[2] | ||
5 | write | 2 | m_ids=[2] | ||
6 | read | 2 | m_ids=[2] | ||
7 | commit | 1 | m_ids=[] | ||
8 | read | 1 | m_ids=[3] |
假設原資料 num=0
tx1 update num=num+1
tx2 update num=num+2
我們來探討一下 tx1 的 time2,5,6 與 tx3 的 8 吧!
time2 跟上面一樣,可成功抓到 num=0,這裡就不多贅述,可以往上翻
我們來解析一下 tx1 time5 的預期效果update num=num+1
在更新之前, 我們再加入一個 select 來看看
select num from ...
update ... set num=num+1
所以會先 select 取得 num 的值來確認一下是 update 之前的 num 是什麼
select num from ...
//原表
{"num":0,"trx_id": 0}<= {"num":0+2,"trx_id": 2}
// ReadView
// 直接抓上次的 {"name":0,"trx_id": 0}
所以可以看到了 num=0, 因為是抓取上次的 ReadView,看起來十分正常,讓我們繼續看下去
update ... set num=${num}+1
上一步取得了 num=0,那麼我想+1, 應該會得到 num=1
//原表 {"num":0,"trx_id": 0} override by tx2
// ↑→→→→→→→→→→→→→→→→→→→→→→→→→↓
{"num":0+2,"trx_id": 2}<= {"num":(0+2)+1,"trx_id": 1}
等等, 怎麼 num 變成了(0+2)+1
呢?說好的 0+1
呢?
怎麼會查出來的值跟預期中的不同? 應該只是暫時不一樣吧?等等下一步 select 出來應該會是說好的
//原表 {"num":0,"trx_id": 0} override by tx2
// ↑→→→→→→→→→→→→→→→→→→→→→→→→→↓
{"num":0+2,"trx_id": 2}<= {"num":(0+2)+1,"trx_id": 1}
// ReadView
// 直接抓上次的{"num":(0+2)+1,"trx_id": 1}
出來結果是 num=(0+2)+1, 對,你沒看錯,變成3完全出乎意料之外, 驚不驚喜意不意外啊!
//原表 {"num":0,"trx_id": 0} {"num":0+2,"trx_id": 2} override by tx1
// ↓←←←←←←←←←←←←←←←←←←←←←←←←←←←↓
{"num":(0+2)+1,"trx_id": 1}
// ReadView
{ "creator_trx_id": "tx3", "m_ids": [3], "up_limit_id": 3, "low_limit_id": 3}
// 取 {"num":(0+2)+1,"trx_id": 1}
條件 1: creator_trx_id(tx2)=trx_id(0) false
條件 2:
[trx_id<=up_limit_id]<___<low_limit_id
, return true結論: readview 可以利用目前表上的資料,直接讀原表該紀錄的尾巴,即可建立 readview, 記下這張 readview
所以出來結果是 num=3 (0+2+1), 完全沒有出現預期中的 num=1
RR 是防止同一列讀第二次數值不同。
理想上 read/write 會抓到同一個版本的值,而非最新 commit 點。
因為 mysql 骨子裡其實還是 sxlock,純 sxlock 遇到 read-write conflict 時,會發生 block
所以透過 readview 來實作 mvcc 的關係,事實上就是快照讀, 當前寫,
因此 mysql 只符合 read version 相同。
postgres 因為是純樂觀鎖所建構出的 mvcc,
在 tx1 time5 會 throw error, 來拒絕 tx1 的 write
問題在於 mysql 在 REPEATABLE READ Level 下, 原 tx 建立了 readview
其他 tx 修改後,原 tx 還能修改同一紀錄或原 tx 修改的版本與 readview 會呈現的版本不同
因此
select ... for update
,將該筆紀錄完全鎖上,避免中間被別人修改任何本質上是 SX lock,但又支援 MVCC 的 RDBMS,
雖然解決的 read-write conflict,
但如果在 REPEATABLE READ level 下,
在做 write-write conflict 時請務必要特別注意喔!